package cn.csc.module.pay.service.refund;

import cn.csc.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.csc.framework.common.pojo.PageResult;
import cn.csc.framework.common.util.number.ObjectNumberUtils;
import cn.csc.framework.pay.core.client.PayClient;
import cn.csc.framework.pay.core.client.PayClientFactory;
import cn.csc.framework.pay.core.client.PayCommonResult;
import cn.csc.framework.pay.core.client.dto.PayNotifyReqDTO;
import cn.csc.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
import cn.csc.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
import cn.csc.framework.pay.core.enums.PayTypeEnum;
import cn.csc.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.csc.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.csc.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
import cn.csc.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
import cn.csc.module.pay.convert.refund.PayRefundConvert;
import cn.csc.module.pay.dal.dataobject.merchant.PayAppDO;
import cn.csc.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.csc.module.pay.dal.dataobject.notify.NotifyRefundLogDO;
import cn.csc.module.pay.dal.dataobject.order.PayOrderDO;
import cn.csc.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.csc.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.csc.module.pay.dal.mysql.order.PayOrderMapper;
import cn.csc.module.pay.dal.mysql.refund.PayRefundMapper;
import cn.csc.module.pay.enums.ErrorCodeConstants;
import cn.csc.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.csc.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.csc.module.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.csc.module.pay.enums.order.PayOrderStatusEnum;
import cn.csc.module.pay.enums.refund.PayRefundStatusEnum;
import cn.csc.module.pay.enums.refund.PayRefundTypeEnum;
import cn.csc.module.pay.framework.pay.config.PayProperties;
import cn.csc.module.pay.service.merchant.PayAppService;
import cn.csc.module.pay.service.merchant.PayChannelService;
import cn.csc.module.pay.service.notify.NotifyRefundLogService;
import cn.csc.module.pay.service.notify.PayNotifyService;
import cn.csc.module.pay.service.order.PayOrderExtensionService;
import cn.csc.module.pay.service.order.PayOrderService;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static cn.csc.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.csc.framework.common.util.json.JsonUtils.toJsonString;
import static cn.csc.module.pay.enums.ErrorCodeConstants.*;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;

/**
 * 退款订单 Service 实现类
 *
 * @author aquan
 */
@Service
@Slf4j
@Validated
public class PayRefundServiceImpl implements PayRefundService {

    @Resource
    private PayProperties payProperties;

    @Resource
    private PayClientFactory payClientFactory;

    @Resource
    private PayRefundMapper refundMapper;
    @Resource
    private PayOrderMapper orderMapper; // TODO @jason：需要改成不直接操作 db；

    @Resource
    private NotifyRefundLogService notifyRefundLogService;

    @Resource
    private PayOrderService orderService;
    @Resource
    private PayOrderExtensionService orderExtensionService;
    @Resource
    private PayAppService appService;
    @Resource
    private PayChannelService channelService;
    @Resource
    private PayNotifyService notifyService;

    @Override
    public PayRefundDO getRefund(BigInteger id) {
        return refundMapper.selectById(id);
    }

    @Override
    public PageResult<PayRefundDO> getRefundPage(PayRefundPageReqVO pageReqVO) {
        return refundMapper.selectPage(pageReqVO);
    }

    @Override
    public List<PayRefundDO> getRefundList(PayRefundExportReqVO exportReqVO) {
        return refundMapper.selectList(exportReqVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public BigInteger createPayRefund(PayRefundCreateReqDTO reqDTO) {
        // 1.1 校验 App
        PayAppDO app = appService.validPayApp(reqDTO.getAppId());
        // 1.2 校验支付订单
        PayOrderDO order = validatePayOrderCanRefund(reqDTO);

        // 1.3 校验支付渠道是否有效
        PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
        // 校验支付客户端是否正确初始化
        PayClient client = payClientFactory.getPayClient(channel.getId());
        if (client == null) {
            log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
            throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
        }

        // 1.4 校验退款订单是否已经存在
        PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId(
                app.getId(), reqDTO.getMerchantRefundNo());
        if (refund != null) {
            throw exception(PAY_REFUND_HAVE);
        }

        // 2.1 插入退款单
        String no = payProperties.getRefundNoPrefix() + IdUtil.getSnowflakeNextId();
        refund = PayRefundConvert.INSTANCE.convert(reqDTO)
                .setNo(no).setOrderId(order.getId()).setTradeNo(order.getNo())
                .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
                // 商户相关的字段
                .setNotifyUrl(app.getRefundNotifyUrl())
                // 渠道相关字段
                .setChannelOrderNo(order.getChannelOrderNo())
                .setNotifyStatus(0)
                .setType(20)  // 全部退款
                // 退款相关字段
                .setStatus(PayRefundStatusEnum.WAITING.getStatus())
                .setPayAmount(order.getAmount()).setRefundAmount(reqDTO.getAmount());
        refundMapper.insert(refund);

        try {
            // 2.2 向渠道发起退款申请
            PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO()
                    .setPayAmount(order.getAmount())
                    .setRefundAmount(reqDTO.getAmount())
                    .setPayTradeNo(order.getNo())
                    .setMerchantRefundId(refund.getNo())
                    .setNotifyUrl(genChannelRefundNotifyUrl(channel))
                    .setReason(reqDTO.getReason());
            PayRefundUnifiedRespDTO refundRespDTO = client.unifiedBeanRefund(unifiedReqDTO);
            log.info("[createPayRefund]退款接口返回: {}",refundRespDTO);
            // 2.3 处理退款返回
            getSelf().doNotifyRefund(channel, refundRespDTO);
        } catch (Throwable e){
            // 注意：这里仅打印异常，不进行抛出。
            // 原因是：虽然调用支付渠道进行退款发生异常（网络请求超时），实际退款成功。这个结果，后续通过退款回调、或者退款轮询补偿可以拿到。
            // 最终，在异常的情况下，支付中心会异步回调业务的退款回调接口，提供退款结果
            log.error("[createPayRefund][退款 id({}) requestDTO({}) 发生异常]",
                    refund.getId(), reqDTO, e);
        }

        // 返回退款编号
        return refund.getId();
    }

    /**
     * 校验支付订单是否可以退款
     *
     * @param reqDTO 退款申请信息
     * @return 支付订单
     */
    private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
        PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
        if (order == null) {
            throw exception(PAY_ORDER_NOT_FOUND);
        }
        // 校验状态，必须是已支付、或者已退款
        if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) {
            throw exception(PAY_ORDER_REFUND_FAIL_STATUS_ERROR);
        }

        log.info("validatePayOrderCanRefund: {} / {}",reqDTO,order);
        // 校验金额，退款金额不能大于原定的金额， refundAmount为订单已退款的总金额
        if (reqDTO.getAmount() + order.getRefundAmount() > order.getAmount()){
            throw exception(PAY_REFUND_AMOUNT_EXCEED);
        }
        // 是否有退款中的订单
        if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
                PayRefundStatusEnum.WAITING.getStatus()) > 0) {
            throw exception(PAY_REFUND_HAVE);
        }
        return order;
    }

    /**
     * 根据支付渠道的编码，生成支付渠道的回调地址
     *
     * @param channel 支付渠道
     * @return 支付渠道的回调地址  配置地址 + "/" + channel id
     */
    private String genChannelRefundNotifyUrl(PayChannelDO channel) {
        return payProperties.getApiCallbackUrl() + "/" + channel.getId();
    }

    /**
     * 通知并更新订单的退款结果
     *
     * @param channel 支付渠道
     * @param notify 通知
     */
    @Transactional(rollbackFor = Exception.class)  // 注意，如果是方法内调用该方法，需要通过 getSelf().notifyRefund(channel, notify) 调用，否则事务不生效
    public void doNotifyRefund(PayChannelDO channel, PayRefundUnifiedRespDTO notify) {
        // 情况一：退款成功
        if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
            notifyRefundSuccess(channel, notify);
            return;
        }
        // 情况二：退款失败
        if (PayRefundStatusRespEnum.isFailure(notify.getStatus())) {
            notifyRefundFailure(channel, notify);
        }
    }

    private void notifyRefundSuccess(PayChannelDO channel, PayRefundUnifiedRespDTO notify) {
        // 1.1 查询 PayRefundDO
        PayRefundDO refund = refundMapper.selectByAppIdAndNo(
                channel.getAppId(), notify.getOutRefundNo());
        if (refund == null) {
            throw exception(PAY_REFUND_NOT_FOUND);
        }
        if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功，直接返回，不用重复更新
            log.info("[notifyRefundSuccess][退款订单({}) 已经是退款成功，无需更新]", refund.getId());
            return;
        }
        if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        // 1.2 更新 PayRefundDO
        PayRefundDO updateRefundObj = new PayRefundDO()
                .setSuccessTime(LocalDateTimeUtil.parse(notify.getFinTime(),NORM_DATETIME_PATTERN))
                .setChannelRefundNo(notify.getChannelRefundId())
                .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
                .setChannelNotifyData(toJsonString(notify));
        int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
        if (updateCounts == 0) { // 校验状态，必须是等待状态
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        log.info("[notifyRefundSuccess][退款订单({}) 更新为退款成功]", refund.getId());

        // 2. 更新订单
        orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundAmount());

        // 3. 插入退款通知记录
        notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(),
                refund.getId(), notify.getOutRefundNo(), notify.getFinTime());
    }

    private void notifyRefundFailure(PayChannelDO channel, PayRefundUnifiedRespDTO notify) {
        // 1.1 查询 PayRefundDO
        PayRefundDO refund = refundMapper.selectByAppIdAndNo(
                channel.getAppId(), notify.getOutRefundNo());
        if (refund == null) {
            throw exception(PAY_REFUND_NOT_FOUND);
        }
        if (PayRefundStatusEnum.isFailure(refund.getStatus())) { // 如果已经是成功，直接返回，不用重复更新
            log.info("[notifyRefundSuccess][退款订单({}) 已经是退款关闭，无需更新]", refund.getId());
            return;
        }
        if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        // 1.2 更新 PayRefundDO
        PayRefundDO updateRefundObj = new PayRefundDO()
                .setChannelRefundNo(notify.getChannelRefundId())
                .setStatus(PayRefundStatusEnum.FAILURE.getStatus())
                .setChannelNotifyData(toJsonString(notify))
                .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg());
        int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
        if (updateCounts == 0) { // 校验状态，必须是等待状态
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId());

        // 2. 插入退款通知记录
        notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(),
                refund.getId(), notify.getOutRefundNo(), notify.getFinTime());
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayCommonResult<PayRefundUnifiedRespDTO> createPayRefundOrder(PayRefundCreateReqDTO reqDTO) {
        // 获得 PayOrderDO
        //PayOrderDO order = orderService.getOrder(reqDTO.getPayOrderId());
        //PayOrderDO order = orderService.getOrderByMerchantOrderId(reqDTO.getMerchantOrderId());
        PayOrderExtensionDO orderExtensionDO = orderExtensionService.getOrderExtensionByNo(reqDTO.getMerchantOrderId());
        PayOrderDO order = orderService.getOrder(orderExtensionDO.getOrderId());
        // 校验订单是否存在
        if (Objects.isNull(order)) {
            throw exception(PAY_ORDER_NOT_FOUND);
        }
        // 校验 App
        PayAppDO app = appService.validPayApp(order.getAppId());
        // 校验支付渠道是否有效
        PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
        // 校验支付客户端是否正确初始化
        PayClient client = payClientFactory.getPayClient(channel.getId());
        if (client == null) {
            log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
            throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
        }

        // TODO 待实现
        //String merchantRefundId = RandomUtil.randomNumbers(16);
        String merchantRefundId = ObjectNumberUtils.getOrder("");

        // 校验退款的条件
        validatePayRefund(reqDTO, order);
        // 退款类型
        PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME;
        if (Objects.equals(reqDTO.getAmount(), order.getAmount())) {
            refundType = PayRefundTypeEnum.ALL;
        }
        //PayOrderExtensionDO orderExtensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
        PayRefundDO payRefundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(),
                reqDTO.getMerchantRefundNo());  // TODO 需要优化
        if (Objects.nonNull(payRefundDO)) {
            // 退款订单已经提交过。
            //TODO 校验相同退款单的金额
            // TODO @jason：咱要不封装一个 ObjectUtils.equalsAny
            if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
                    || Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
                //已成功退款
                throw exception(ErrorCodeConstants.PAY_REFUND_SUCCEED);
            } else {
                throw exception(ErrorCodeConstants.PAY_REFUND_HAVE);
            }

            //可以重复提交，保证 退款请求号 一致，由渠道保证幂等
        } else {
            // 成功，插入退款单 状态为生成.没有和渠道交互
            // TODO @jason：搞到 convert 里。一些额外的自动，手动 set 下；
            payRefundDO = PayRefundDO.builder()
                    .appId(order.getAppId())
                    .channelOrderNo(order.getChannelOrderNo())
                    .channelCode(order.getChannelCode())
                    .channelId(order.getChannelId())
                    .merchantId(order.getMerchantId())
                    .orderId(order.getId())
                    .merchantRefundNo(reqDTO.getMerchantRefundNo()) // TODO 需要优化
                    .notifyUrl(app.getRefundNotifyUrl())
                    .payAmount(order.getAmount())
                    .refundAmount(reqDTO.getAmount())
                    .userIp(reqDTO.getUserIp())
                    .merchantOrderId(order.getMerchantOrderId())
                    .tradeNo(orderExtensionDO.getNo())
                    .status(PayRefundStatusEnum.WAITING.getStatus())
                    .reason(reqDTO.getReason())
                    .notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
                    .type(refundType.getStatus())
                    .build();
            refundMapper.insert(payRefundDO);
        }
        // TODO @jason：搞到 convert 里。一些额外的自动，手动 set 下；
        PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
        unifiedReqDTO.setUserIp(reqDTO.getUserIp())
                .setRefundAmount(reqDTO.getAmount())
                .setChannelOrderNo(order.getChannelOrderNo())
                .setMerchantOrderId(order.getMerchantOrderId())
                .setPayTradeNo(orderExtensionDO.getNo())
                .setMerchantRefundId(reqDTO.getMerchantRefundNo())  // TODO 需要优化
                .setNotifyUrl(genChannelApiPayNotifyUrl(channel)) // TODO 优化下 notifyUrl
                .setReason(reqDTO.getReason());
        // 向渠道发起退款申请
        PayCommonResult<PayRefundUnifiedRespDTO> result = client.unifiedRefund(unifiedReqDTO);
        if (GlobalErrorCodeConstants.SUCCESS.getCode() == result.getCode()) {
            PayRefundUnifiedRespDTO respDTO = result.getData();
            if (respDTO != null) {
                if (PayTypeEnum.TLPAY.getStatus() == result.getData().getRefundType()) {
                    Map<String, String> param = new HashMap<>();
                    StringBuffer buffer = new StringBuffer();
                    try {
                        //通联银联平台交易号
                        param.put("trxid", respDTO.getChannelRefundId());
                        //本地系统生成的唯一订单扩展单号
                        param.put("cusorderid", orderExtensionDO.getNo());
                        //退款订单号相当于通联的reqsn字段
                        param.put("merchantrefundno", payRefundDO.getMerchantRefundNo());
                        //退款标识
                        param.put("refundfee", "true");
                        //退款完成时间
                        param.put("paytime", respDTO.getFinTime());
                        param.entrySet().forEach(s -> {
                            buffer.append("&").append(s.getKey()).append("=").append(s.getValue());
                        });
                        PayNotifyReqDTO rawNotify = PayNotifyReqDTO.builder().params(param).body(buffer.toString()).build();
                        Object notify = client.parseNotify(rawNotify);
                        // 退款通知
                        notifyRefund(channel.getId(), (PayRefundUnifiedRespDTO) notify);
                    } catch (Exception e) {
                        log.error("----退款失败:{}----", toJsonString(param), e);
                        param.put("notifyUrl", unifiedReqDTO.getNotifyUrl());
                        NotifyRefundLogDO logDO = NotifyRefundLogDO.builder().notifyUrl(unifiedReqDTO.getNotifyUrl())
                                .status(PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()).notifyTimes(1).refundOrderId(payRefundDO.getOrderId())
                                .response(buffer.toString()).orderId(order.getId()).build();
                        notifyRefundLogService.insert(logDO);
                    }

                }
            }
        }
        return result;
    }

    /**
     * 根据支付渠道的编码，生成支付渠道的回调地址
     *
     * @param channel 支付渠道
     * @return 支付渠道的回调地址  配置地址 + "/" + channel id
     */
    private String genChannelPayNotifyUrl(PayChannelDO channel) {
        return payProperties.getCallbackUrl() + "/" + channel.getId();
    }

    /**
     * 根据支付渠道的编码，生成支付渠道的回调地址
     *
     * @param channel 支付渠道
     * @return 支付渠道的回调地址  配置地址 + "/" + channel id
     */
    private String genChannelApiPayNotifyUrl(PayChannelDO channel) {
        return payProperties.getApiCallbackUrl() + "/" + channel.getId();
    }

    @Override
    public void notifyRefund(BigInteger channelId, PayRefundUnifiedRespDTO notify) {
        log.info("notifyRefund退款通知: {}",notify);
        // 校验支付渠道是否有效
        PayChannelDO channel = channelService.validPayChannel(channelId);
        // 更新退款订单
        getSelf().doNotifyRefund(channel, notify);
    }


    /**
     * 校验是否进行退款
     *
     * @param reqDTO 退款申请信息
     * @param order  原始支付订单信息
     */
    private void validatePayRefund(PayRefundCreateReqDTO reqDTO, PayOrderDO order) {
        // 校验状态，必须是支付状态
        if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
            throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
        }
        // 是否已经全额退款
        if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
            throw exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);
        }
        // 校验金额 退款金额不能大于 原定的金额
        if (reqDTO.getAmount() + order.getRefundAmount() > order.getAmount()) {
            throw exception(PAY_REFUND_AMOUNT_EXCEED);
        }
        // 校验渠道订单号
        if (StrUtil.isEmpty(order.getChannelOrderNo())) {
            throw exception(ErrorCodeConstants.PAY_REFUND_CHN_ORDER_NO_IS_NULL);
        }
        //TODO  退款的期限  退款次数的控制
    }

    /**
     * 获得自身的代理对象，解决 AOP 生效问题
     *
     * @return 自己
     */
    private PayRefundServiceImpl getSelf() {
        return SpringUtil.getBean(getClass());
    }

}
